﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;
using System.Threading;
using MacomberMapSystem.Common.Database;
using System.Drawing.Imaging;
using System.Net;
using System.Windows.Forms;
using MacomberMapSystem.Common.Integration;

namespace MacomberMapSystem.Common.User_Interfaces.Mapping
{
    /// <summary>
    /// This class holds the information on a display tile
    /// </summary>
    public class MM_MapTile
    {

        #region Static variable declarations        
        /// <summary>The google map version for map</summary>
        public static string VersionGoogleMap = "w2.92";

        /// <summary>The google map version for satellite</summary>
        public static string VersionGoogleSatellite = "38";

        /// <summary>The google map version for labels</summary>
        public static string VersionGoogleLabels = "w2t.92";

        /// <summary>The google map version for terrain</summary>
        public static string VersionGoogleTerrain = "w2p.87";

        /// <summary>The google map security word</summary>
        public static string SecGoogleWord = "Galileo";

        /// <summary>The yahoo map version for map</summary>
        public static string VersionYahooMap = "4.2";

        /// <summary>The yahoo map version for satellite</summary>
        public static string VersionYahooSatellite = "1.9";

        /// <summary>The yahoo map version for labels</summary>
        public static string VersionYahooLabels = "4.2";

        /// <summary>The virtual earth version</summary>
        public static string VersionVirtualEarth = "244";
      
        /// <summary>Our initial/blank bitmap</summary>
        public static Bitmap BlankBitmap = null;

        /// <summary>The server to be used for this request</summary>
        public static int ServerNumber = 0;

        /// <summary>Our collection of map tiles to be loaded</summary>
        public static Queue<MM_MapTile> ToDownload = new Queue<MM_MapTile>();

        /// <summary>Our thread for tile downloading</summary>
        public static Thread TileDownloader = null;
        #endregion

        #region Variable Declaration
        /// <summary>Our actual tile image</summary>
        public Bitmap Tile;

        /// <summary>The X/Y coordinates of our tile</summary>
        public Point TileXY;

        /// <summary>The top-left point of our tile (in Lat/Long)</summary>
        public PointF TopLeft;

        /// <summary>The bottom-right point of our tile (in Lat/Long)</summary>
        public PointF BottomRight;

        /// <summary>The top-left point of our tile (in pixels)</summary>
        public PointF TopLeftXY;

        /// <summary>The bottom-right point of our tile (in pixels)</summary>
        public PointF BottomRightXY;

        /// <summary>The map type used by this rendering</summary>
        public MapType mapType;

        /// <summary>The zoom level of the tile</summary>
        public int ZoomLevel;

        /// <summary>The bitmap is saving</summary>
        public bool Saving = false;

        /// <summary>The bitmap has failed to download</summary>
        public bool Failed = false;

        /// <summary>
        /// The type of maps we have available
        /// </summary>
        public enum MapType
        {
            /// <summary>Do not display map tiles</summary>
            None = 0,

            /// <summary>Google map</summary>
            GoogleMap = 1,

            /// <summary>Google satellite</summary>
            GoogleSatellite = 4,

            /// <summary>Google labels</summary>
            GoogleLabels = 8,

            /// <summary>Google terrain</summary>
            GoogleTerrain = 16,

            /// <summary>Google hybrid</summary>
            GoogleHybrid = 20,

            /// <summary>Google terrain hybrid</summary>
            GoogleTerrainHybrid=21,

            /// <summary>Open Street Map</summary>
            OpenStreetMap = 32,

            /// <summary>Open Stree OSM</summary>
            OpenStreetOsm = 33,

            /// <summary>Yahoo map</summary>
            YahooMap = 64,

            /// <summary>Yahoo satellite</summary>
            YahooSatellite = 128,

            /// <summary>Yahoo labels</summary>
            YahooLabels = 256,

            /// <summary>Yahoo hybrid</summary>
            YahooHybrid = 333,

            /// <summary>Virtual Earth map</summary>
            VirtualEarthMap = 444,

            /// <summary>Virtual Earth satellite</summary>
            VirtualEarthSatellite = 555,

            /// <summary>Virtual Earth hybrid</summary>
            VirtualEarthHybrid = 666,
        }

        #endregion

        #region Initialization
        /// <summary>
        /// Load in a new tile
        /// </summary>
        /// <param name="mapType">Our map type</param>
        /// <param name="TileXY">Our tile coordinates</param>
        /// <param name="ZoomLevel">Our current zoom level</param>
        /// <param name="DbConnection">The database connection</param>
        public MM_MapTile(MapType mapType, Point TileXY, int ZoomLevel, MM_Database_Connector DbConnection)
        {
            this.mapType = mapType;
            //Check to make sure we have our blank bitmap
            if (BlankBitmap == null)
            {
                BlankBitmap = new Bitmap(256, 256);
                using (Graphics g = Graphics.FromImage(BlankBitmap))
                {
                    g.Clear(Color.Black);
                    g.DrawLine(Pens.Red, 0, 0, 255, 255);
                    g.DrawLine(Pens.Red, 255, 0, 0, 255);
                }
            }

            //Determine our tile's positioning
            this.TileXY = TileXY;
            this.ZoomLevel = ZoomLevel;
            this.TopLeft = MM_NetworkMap_Display.TileTopLeft(TileXY, ZoomLevel).ToPointF();
            this.BottomRight = MM_NetworkMap_Display.TileBottomRight(TileXY, ZoomLevel).ToPointF();
            this.TopLeftXY = new Point(TileXY.X * MM_NetworkMap_Display.TileSize.Width, TileXY.Y * MM_NetworkMap_Display.TileSize.Height);
            this.BottomRightXY = new Point(((TileXY.X + 1) * MM_NetworkMap_Display.TileSize.Width) - 1, ((TileXY.Y + 1) * MM_NetworkMap_Display.TileSize.Height) - 1);
            this.Tile = BlankBitmap;

            //Add this tile to our downloader
            if (TileDownloader == null)
            {
                TileDownloader = new Thread(TileDownloadLoop);
                TileDownloader.Name = "Tile downloader.";
                TileDownloader.Priority = ThreadPriority.BelowNormal;
                TileDownloader.SetApartmentState(ApartmentState.MTA);
                ToDownload.Enqueue(this);
                TileDownloader.Start(DbConnection);
            }
            else            
                ToDownload.Enqueue(this);
        }

        /// <summary>
        /// Queue this tile for download
        /// </summary>
        public void Queue()
        {
            this.Failed = false;
            ToDownload.Enqueue(this);
        }

        /// <summary>
        /// Our static loop for downloading the tiles
        /// </summary>
        /// <param name="state"></param>
        private static void TileDownloadLoop(object state)
        {
            MM_Database_Connector DbConnection = state as MM_Database_Connector;
            MM_MapTile InTile=null;
            while (true)
            {
                if (Application.OpenForms.Count == 0)
                    return;
                if (ToDownload.Count == 0)
                    Thread.Sleep(250);
                else
                    try
                    {
                        InTile = ToDownload.Dequeue();
                        InTile.LoadTile(DbConnection);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine("Error in tile download: " + ex.Message);
                        InTile.Failed = true;
                    }
            }
        }        
        #endregion

        #region Tile downloader

        /// <summary>
        /// Load a tile from the internet
        /// </summary>
        /// <param name="DbConnection">The database connection, if any</param>
        private void LoadTile(MM_Database_Connector DbConnection)
        {
            if (!DbConnection.LoadMapTile(this.mapType, this.TileXY, this.ZoomLevel, out Tile))
            {

                if (mapType == MapType.GoogleHybrid)
                    Tile = BlendBitmaps(MapType.GoogleSatellite, MapType.GoogleLabels, TileXY, ZoomLevel, 0.95f);
                if (mapType == MapType.GoogleTerrainHybrid)
                    Tile = BlendBitmaps(MapType.GoogleTerrain, MapType.GoogleLabels, TileXY, ZoomLevel, 0.95f);
                else if (mapType == MapType.YahooHybrid)
                    Tile = BlendBitmaps(MapType.GoogleSatellite, MapType.GoogleLabels, TileXY, ZoomLevel, 0.95f);
                else
                    Tile = DownloadImage(mapType, TileXY, ZoomLevel);

                this.Saving = true;
                DbConnection.SaveMapTile(this);
                this.Saving = false;
            }
            if (TileUpdated != null)
                TileUpdated(this);
        }

        /// <summary>
        /// Return a bitmap consisting of two blended bitmaps
        /// </summary>
        /// <param name="mapType1">The first map type</param>
        /// <param name="mapType2">The second map type</param>
        /// <param name="TileXY">The tile's X/Y</param>
        /// <param name="ZoomLevel">The tile's zoom level</param>
        /// <param name="Transparency">The tile's transparency</param>
        /// <returns></returns>
        private Bitmap BlendBitmaps(MM_MapTile.MapType mapType1, MM_MapTile.MapType mapType2, Point TileXY, int ZoomLevel, float Transparency)
        {                       
            //Prepare our transparency matrix
            ColorMatrix OutMatrix = new ColorMatrix(new float[][] { new float[] { 1, 0, 0, 0, 0 }, new float[] { 0, 1, 0, 0, 0 }, new float[] { 0, 0, 1, 0, 0 }, new float[] { 0, 0, 0, Transparency, 0 }, new float[] { 0, 0, 0, 0, 1 } });
            ImageAttributes iA = new ImageAttributes();
            iA.SetColorMatrix(OutMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

            //Now apply our images
            Bitmap OutImage = new Bitmap(MM_NetworkMap_Display.TileSize.Width, MM_NetworkMap_Display.TileSize.Height);
            using (Graphics g = Graphics.FromImage(OutImage))
            {
                g.DrawImage(DownloadImage(mapType1, TileXY, ZoomLevel), new Rectangle(0, 0, OutImage.Width, OutImage.Height), 0, 0, OutImage.Width, OutImage.Height, GraphicsUnit.Pixel, iA);
                g.DrawImage(DownloadImage(mapType2, TileXY, ZoomLevel), new Rectangle(0, 0, OutImage.Width, OutImage.Height), 0, 0, OutImage.Width, OutImage.Height, GraphicsUnit.Pixel, iA);
            }
            return OutImage;
        }


        /// <summary>        
        /// Download an image
        /// </summary>
        /// <param name="mapType">The map type to download</param>
        /// <param name="TileXY">The tile's X and Y coordinates</param>
        /// <param name="ZoomLevel">The tile's zoom level</param>
        /// <returns></returns>
        private Bitmap DownloadImage(MM_MapTile.MapType mapType, Point TileXY, int ZoomLevel)
        {
            //Prepare our query
            HttpWebRequest wRq = (HttpWebRequest)HttpWebRequest.Create(MakeImageUrl(mapType, TileXY, ZoomLevel, "en"));
            wRq.UserAgent = "Opera/9.62 (Windows NT 5.1; U; en) Presto/2.1.1";
            wRq.Accept = "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1";
            wRq.Headers["Accept-Encoding"] = "deflate, gzip, x-gzip, identity, *;q=0";
            wRq.Referer = "http://maps.google.com/";
            wRq.KeepAlive = true;
            wRq.Timeout = 1000;

            WebResponse wResp = wRq.GetResponse();
            using (Bitmap InBitmap = (Bitmap)Bitmap.FromStream(wResp.GetResponseStream()))            
                return new Bitmap(InBitmap);
            
        }


        /// <summary>
        /// A delegate handling the updating of a tile
        /// </summary>
        /// <param name="UpdatedTile">The tile that has been updated</param>
        public delegate void TileUpdatedDelegate(MM_MapTile UpdatedTile);

        /// <summary>
        /// An event signifying the tile being update
        /// </summary>
        public event TileUpdatedDelegate TileUpdated;

        /// <summary>
        /// Create a URL for an image
        /// </summary>
        /// <param name="type">The map type</param>
        /// <param name="pos">The tile's X/Y</param>
        /// <param name="zoom">The tile's zoom</param>
        /// <param name="language">The tile's language</param>
        /// <returns></returns>
        private string MakeImageUrl(MapType type, Point pos, int zoom, string language)
        {
            string server = string.Empty;
            string request = string.Empty;
            string version = string.Empty;
            int servernum = ServerNumber;
            ServerNumber = (ServerNumber + 1) % 4;

            switch (type)
            {
                case MapType.GoogleMap:
                    server = "mt";
                    request = "vt/lyrs=m@156";
                    version = VersionGoogleMap;
                    break;

                case MapType.GoogleSatellite:
                    server = "khm";
                    request = "kh/v=87";
                    version = VersionGoogleSatellite;
                    break;

                case MapType.GoogleLabels:
                    server = "mt";
                    request = "vt/lyrs=h";
                    version = VersionGoogleLabels;
                    break;

                case MapType.GoogleTerrain:
                    server = "mt";
                    request = "vt/lyrs=t";
                    version = VersionGoogleTerrain;
                    break;

                case MapType.YahooMap:
                    {
                        return string.Format("http://us.maps2.yimg.com/us.png.maps.yimg.com/png?v={0}&x={1}&y={2}&z={3}&r=1", VersionYahooMap, pos.X.ToString(), (((1 << zoom) >> 1) - 1 - pos.Y).ToString(), (zoom + 1).ToString());
                    }

                case MapType.YahooSatellite:
                    {
                        return string.Format("http://us.maps3.yimg.com/aerial.maps.yimg.com/png?v={0}&t=a&s=256&x={1}&y={2}&z={3}&r=1", VersionYahooSatellite, pos.X.ToString(), (((1 << zoom) >> 1) - 1 - pos.Y).ToString(), (zoom + 1).ToString());
                    }

                case MapType.YahooLabels:
                    {
                        return string.Format("http://us.maps1.yimg.com/us.tile.maps.yimg.com/tl?v={0}&t=h&x={1}&y={2}&z={3}&r=1", VersionYahooLabels, pos.X.ToString(), (((1 << zoom) >> 1) - 1 - pos.Y).ToString(), (zoom + 1).ToString());
                    }

                case MapType.OpenStreetMap:
                    {
                        char letter = "abca"[servernum];
                        return string.Format("http://{0}.tile.openstreetmap.org/{1}/{2}/{3}.png", letter, zoom.ToString(), pos.X.ToString(), pos.Y.ToString());
                    }

                case MapType.OpenStreetOsm:
                    {
                        char letter = "abca"[servernum];
                        return string.Format("http://{0}.tah.openstreetmap.org/Tiles/tile/{1}/{2}/{3}.png", letter, zoom.ToString(), pos.X.ToString(), pos.Y.ToString());
                    }

                case MapType.VirtualEarthMap:
                    {
                        string key = TileXYToQuadKey(pos.X, pos.Y, zoom);
                        return string.Format("http://r{0}.ortho.tiles.virtualearth.net/tiles/r{1}.png?g={2}&mkt={3}", servernum, key, VersionVirtualEarth, language);
                    }

                case MapType.VirtualEarthSatellite:
                    {
                        string key = TileXYToQuadKey(pos.X, pos.Y, zoom);
                        return string.Format("http://a{0}.ortho.tiles.virtualearth.net/tiles/a{1}.jpeg?g={2}&mkt={3}", servernum, key, VersionVirtualEarth, language);
                    }

                case MapType.VirtualEarthHybrid:
                    {
                        string key = TileXYToQuadKey(pos.X, pos.Y, zoom);
                        return string.Format("http://h{0}.ortho.tiles.virtualearth.net/tiles/h{1}.jpeg?g={2}&mkt={3}", servernum, key, VersionVirtualEarth, language);
                    }
            }

            //string sec1 = ""; // after &x=...
            string sec2 = ""; // after &zoom=...
            int seclen = ((pos.X * 3) + pos.Y) % 8;
            sec2 = SecGoogleWord.Substring(0, seclen);
            //if (pos.Y >= 10000 && pos.Y < 100000)            
            //    sec1 = "&s=";
            
            return string.Format("http://{0}{1}.google.com/{2}&hl={3}&x={4}&y={5}&z={6}&s={7}",
                server, servernum, request,language, pos.X, pos.Y, zoom,sec2 );

            //return string.Format("http://{0}{1}.google.com/{2}?v={3}&hl={4}&x={5}{6}&y={7}&z={8}&s={9}", 
            //    server, servernum.ToString(), request, version, language, pos.X.ToString(), sec1, pos.Y.ToString(), zoom.ToString(), sec2);
        }
        /// <summary>
        /// Converts tile XY coordinates into a QuadKey at a specified level of detail.
        /// </summary>
        /// <param name="tileX">Tile X coordinate.</param>
        /// <param name="tileY">Tile Y coordinate.</param>
        /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
        /// to 23 (highest detail).</param>
        /// <returns>A string containing the QuadKey.</returns>
        public static string TileXYToQuadKey(int tileX, int tileY, int levelOfDetail)
        {
            StringBuilder quadKey = new StringBuilder();
            for (int i = levelOfDetail; i > 0; i--)
            {
                char digit = '0';
                int mask = 1 << (i - 1);
                if ((tileX & mask) != 0)
                {
                    digit++;
                }
                if ((tileY & mask) != 0)
                {
                    digit++;
                    digit++;
                }
                quadKey.Append(digit);
            }
            return quadKey.ToString();
        }
        #endregion

        #region Master Tile downloader
        /// <summary>
        /// Download all map tiles
        /// </summary>
        /// <param name="MapServer">The map server to connect to</param>
        /// <param name="StateBoundary">The state boundary</param>
        /// <param name="MinZoom">The minimum zoom level</param>
        /// <param name="MaxZoom">The maximum zoom level</param>
        /// <param name="DbConnection">The database connection to be used to upload the tiles (if any)</param>
        public static void DownloadAllTiles(MapType MapServer, MM_Boundary StateBoundary, int MinZoom, int MaxZoom, MM_Database_Connector DbConnection)
        {
            DateTime StartTime = DateTime.Now;
            int NumTiles = 0;
            for (int Zoom = MinZoom; Zoom <= MaxZoom; Zoom++)
            {
                Point TopLeft = MM_NetworkMap_Display.XYToTile(MM_NetworkMap_Display.LatLngToXY(StateBoundary.Min, Zoom));
                Point BottomRight = MM_NetworkMap_Display.XYToTile(MM_NetworkMap_Display.LatLngToXY(StateBoundary.Max, Zoom));
                for (int x = TopLeft.X; x != BottomRight.X; x += Math.Sign(BottomRight.X - TopLeft.X))
                    for (int Y = TopLeft.Y; Y != BottomRight.Y; Y += Math.Sign(BottomRight.Y - TopLeft.Y))
                    {
                        MM_MapTile NewTile = new MM_MapTile(MapServer, new Point(x, Y), Zoom, null);
                        if (DbConnection != null)
                            DbConnection.SaveMapTile(NewTile);
                        NumTiles++;
                    }
            }

            MessageBox.Show("Downloaded " + NumTiles.ToString("#,##0") + " " + MapServer.ToString() + " tiles in " + (DateTime.Now - StartTime), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
        #endregion

        /// <summary>
        /// Report our tile name
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return TileXY.X + "," + TileXY.Y + "," + mapType.ToString();
        }
    }
}
